﻿
using System;
using System.Collections.Generic;
using Test30.Core;
using Test30.Util;

namespace Test30.Engine
{
    public class Hero : Entity
    {
        public static Hero CreateTemp()
        {
            return new Hero("temp", "A", new Stats(), false);
        }
        public HeroClass Class { get; }
        public override string Name { get; }
        public Equipment Equipment { get; }
        public Inventory Inventory { get; }
        public FluidStat Experience { get; }
        public Stats Stats { get; }
        public int Level { get; private set; }
        public int DeepestDepth { get; private set; }
        public string Race { get; }
        public List<string> SlainUniques{ get; }
        public override int LightRadius
        {
            get
            {
                // start at zero so the hero is always visible
                int radius = 0;

                foreach (Item item in Equipment)
                {
                    if ((item != null) && (item.LightRadius > 0))
                    {
                        radius += item.LightRadius;
                    }
                }

                return radius;
            }
        }
        public float MeleeDamageBonus => GetDamageBonus(true);
        public int MeleeStrikeBonus => GetStrikeBonus(true);

        public Hero(string name, string race, Stats stats, bool cheatDeath) : base(Vec.Zero, Energy.NormalSpeed, 10)
        {
            Name = name;
            Race = race;

            Class = new Warrior();
            Stats = stats;
            _cheatDeath = cheatDeath;

            Inventory = new Inventory();
            Equipment = new Equipment();
            Equipment.ItemEquipped.Add(Equipment_ItemEquipped);

            Level = 1;

            SetBehavior(new OneShotBehavior(null));

            Experience = new FluidStat(0);
            Experience.Changed += Experience_Changed;

            foreach (Stat stat in Stats)
            {
                stat.Changed += Stat_Changed;
            }

            SlainUniques = new List<string>();
            _statGains = new Stat[MaxLevel];
            for (int i = 0; i < _statGains.Length; i++)
            {
                _statGains[i] = Rng.Item(Stats);
            }
            RefreshMaxHealth();
            RefreshAutoHealTimer();
        }

        public bool HasSlain(Race race)
        {
            if (!race.IsUnique) return false;
            return _slainUniques.Contains(race.Name);
        }
        private int GetExperience(int level)
        {
            // offset so that level 1 = 0 experience
            level -= 1;

            return (40 * level * level);
        }

        public void GainExperience(Action action, float experience)
        {
            int experienceCents = (int)(experience * 100);

            if (!Experience.IsMax)
            {
                // experience is drained, so only raise the max a little
                Experience.Base += experienceCents / 5;
            }
            else
            {
                // not drained, so raise the full amount
                Experience.Base += experienceCents;
            }

            Experience.Current += experienceCents;

            RefreshLevel();
        }
        public override Attack GetAttack(Entity defender)
        {
            // default to barehanded
            Roller damage = Roller.Dice(1, 3);
            Element element = Element.Anima;
            string verb = Verbs.Hit;
            EffectType effectType = EffectType.Hit;
            IFlagCollection flags = new FlagCollection();

            // find the equipped melee weapon
            Item weapon = Equipment.MeleeWeapon;
            if (weapon != null)
            {
                damage = weapon.Attack.Damage;
                element = weapon.Attack.Element;
                verb = weapon.Attack.Verb;
                flags = weapon.Flags;

                //### bob: temp. need to get from weapon data
                effectType = EffectType.Stab;
            }

            var attack = new Attack(damage, MeleeStrikeBonus, MeleeDamageBonus, element, verb, effectType, flags);

            // give the class a chance to modify it
            Class.BeforeAttack(defender, attack);

            return attack;
        }
        public float GetExperience(NotNull<Race> race)
        {
            return race.Value.Experience / Level;
        }

        public int GetNumResists(Element element)
        {
            int resists = 0;
            foreach (Item item in Equipment)
            {
                if ((item != null) && item.Resists(element))
                {
                    resists++;
                }
            }
            return resists;
        }

        public void Killed(Action action, Monster monster)
        {
            GainExperience(action, GetExperience(monster.Race));

            // handle killing a unique
            if (monster.Race.IsUnique)
            {
                SlainUniques.Add(monster.Race.Name);
            }

            // handle killing a unique
            if (monster.Race.IsUnique)
            {
                _slainUniques.Add(monster.Race.Name);
            }
            // let the class know
            Class.KilledMonster(action, monster);
        }

        public void NoteDepth(int depth)
        {
            DeepestDepth = Math.Max(DeepestDepth, depth);
        }

        public void SetNextAction(NotNull<Action> action)
        {
            SetBehavior(new OneShotBehavior(action));
        }

        protected override float OnGetResistance(Element element)
        {
            float resist = 1.0f / (1.0f + GetNumResists(element));

            return resist;
        }

        protected override bool OnDie(Action action)
        {
            if (!_cheatDeath)
            {
                Dungeon.Game.Lose();
            }
            else
            {
                action.AddAction(new HealFullAction(this));
            }

            // hero can't actually be removed from game
            return false;
        }


        private void RefreshLevel()
        {
            int level = GetLevel(Experience.Current);

            // lose levels
            while (Level > level)
            {
                Level--;
                // lose a stat
                Stat stat = _statGains[Level + 1]; // offset by one to get the level just lost
                stat.Base--;
            }

            // gain levels
            while (Level < level)
            {
                Level++;
                // gain a stat
                Stat stat = _statGains[Level];
                stat.Base++;
            }
        }
        private void RefreshMaxHealth()
        {
            int change = Stats.Stamina.MaxHealth - Health.Base;

            if (change != 0)
            {
                // set the new base
                Health.Base = Stats.Stamina.MaxHealth;

                // if the max went up, increase the current health too
                if (change > 0)
                {
                    Health.Current += change;
                }
            }

            RefreshAutoHealTimer();
        }
        private void AutoHeal()
        {
            Health.Current++;
        }
        private int GetLevel(int experienceCents)
        {
            int level = 1;

            while (level <= MaxLevel)
            {
                int needed = GetExperience(Level + 1);
                int neededCents = needed * 100;

                // stop if we don't have enough
                if (experienceCents < neededCents) break;

                level++;
            }

            return level;
        }
        private float GetDamageBonus(bool melee)
        {
            // get the bonuses from equipment
            float damageBonus = 1.0f;
            foreach (Item item in Equipment.GetEquipped(melee, !melee))
            {
                damageBonus *= item.DamageBonus;
            }

            // stat bonuses
            damageBonus *= Stats.Strength.DamageBonus;

            //### bob: need to include temporary magical bonuses

            return damageBonus;
        }
        private int GetStrikeBonus(bool melee)
        {
            // get the bonuses from equipment
            int strikeBonus = 0;
            foreach (Item item in Equipment.GetEquipped(melee, !melee))
            {
                strikeBonus += item.StrikeBonus;
            }

            // stat bonuses
            strikeBonus += Stats.Agility.StrikeBonus;

            //### bob: need to include temporary magical bonuses

            return strikeBonus;
        }

        private void RefreshAutoHealTimer()
        {
            // get rid of the old timer
            if (_autoHealTimer != null)
            {
                Energy.Timers.Remove(_autoHealTimer);
            }

            // heals faster the greater the max health
            int stepsPerHeal = Math.Max(1, 400 / Health.Max);

            _autoHealTimer = new EnergyTimer(AutoHeal, stepsPerHeal, true);
            Energy.Timers.Add(_autoHealTimer);
        }

        #region Event Members

        private void Equipment_ItemEquipped(Item item, EventArgs e)
        {
            // refresh the stat bonuses
            foreach (Stat stat in Stats)
            {
                int bonus = 0;

                foreach (Item thisItem in Equipment)
                {
                    if (thisItem != null)
                    {
                        bonus += thisItem.GetStatBonus(stat);
                    }
                }

                stat.SetBonus(BonusType.Equipment, bonus);
            }

            // refresh the speed bonus
            int speedBonus = 0;
            foreach (Item thisItem in Equipment)
            {
                if (thisItem != null)
                {
                    speedBonus += thisItem.SpeedBonus;
                }
            }

            Speed.SetBonus(BonusType.Equipment, speedBonus);

            // refresh the lighting (in case it was a light source)
            OnLightRadiusChanged();

            OnChanged();
        }
        private void Experience_Changed(object sender, EventArgs e)
        {
            OnChanged();
        }
        private void Stat_Changed(object sender, EventArgs e)
        {
            OnChanged();

            // in case stamina changed
            RefreshMaxHealth();
        }
        #endregion

        #region INoun Members

        public override string NounText => "you";
        public override Person Person => Person.Second;
        public override string Pronoun => "you";
        public override string Possessive => "your";

        #endregion

        private EnergyTimer _autoHealTimer;
        private readonly bool _cheatDeath;
        private readonly Stat[] _statGains;
        private readonly List<string> _slainUniques = new List<string>();

        private const int MaxLevel = 50;
    }
}
